#!/usr/bin/env python
#
# Copyright 2008 Guillaume Bort (http://guillaume.bort.fr)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""play.py for google app engine

play.py is an extension module for the WSGI-compatible web application framework provided by Google App Engine. It provides some nice features that I needed in order to move my applications to App Engine :

    * URL patterns with named arguments and HTTP method association (routes).
    * Controller/Action pattern instead of the simpler Handler pattern.
    * Before/After action interceptor (very usefull for authentication).
    * Result methods which break the control flow.
    * Session & Flash scopes support with secure cookie based storage.
    * Better errors report. 
    
Check the reworked 'Getting Started' at http://code.google.com/p/play-py/
"""

import logging
import base64
import hashlib
import hmac
import re
import urlparse
import sys
import os
import traceback
import wsgiref.handlers

from UserDict import UserDict

from google.appengine.ext import webapp
from google.appengine.ext.webapp import template

# PUT A VERY SECRET KEY HERE
SECRET_KEY = '''1d5eh91wxueg5ns4t8'''


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Application

class Application:

	def __init__(self, routes):		
		self.router = Router(routes)
		wsgiref.handlers.CGIHandler().run(self)
		
	def __call__(self, environ, start_response):
		request = Request(environ)
		response = Response()
		
		(controller, action) = self.router.route(request)
		
		if controller and action:
			controller.request, controller.response = request, response
			controller.session = Session(controller)
			controller.flash = Flash(controller)
			controller.params = Params(controller)
			controller.render_args = {}
			
			# Retrieve args
			argnames = action.im_func.func_code.co_varnames[1:action.im_func.func_code.co_argcount]
			args = {}
			for argname in argnames:
				args[argname] = controller.params[argname]
			
			# OK, call the action (with before and after)
			try:
				controller._veryBefore()
				result = None
				if hasattr(controller, '_before'): 
					result = controller._before()
				if not result:
					result = action(controller, **args)
				if result:
					result.do(controller)
				if hasattr(controller, '_after'): controller._after()
				controller._veryAfter()
			except:
				(type, value, tb) = sys.exc_info()
				logging.error(''.join(traceback.format_exception(*sys.exc_info())))
				report_error(type, value, tb, response)
		
		else:
			response.set_status(404)
		
		response.wsgi_write(start_response)
		return ['']
		
		
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Routes
		
class Router:

	def __init__(self, routes):
		Router.current = self
		self.routes = []
		self.actions = {}
		for pattern, action in routes:
			route = Route(pattern, action)
			self.routes.append(route)
			self.actions['%s.%s' % (route.action.im_class.__name__, route.action.__name__)] = route
	
	def route_for_action(self, action):
		return self.actions.get(action)
		
	def route(self, request):
		for route in self.routes:
			params = route.match(request)
			if params != None:				
				action = route.action
				controller = action.im_class()
				controller.router = self
				controller._route_params = params
				return (controller, action)
		return (None, None)
		
	def reverse_route(self, func, args):
		for route in self.routes:
			if route.method == 'get' and route.action == func:
				return route.resolve(args)
			
			
class Route:

	def __init__(self, pattern, action):
		self.description = str(pattern)
		self.action = action
		self.method = 'all'
		if pattern.index(' '):
			(self.method, self.path) = tuple(pattern.split())
			self.method = self.method.lower()
			pattern = self.path
		p = re.compile(r'\{([a-zA-Z]+)\}')
		pattern = '^' + p.sub(r'(?P<\1>[^/]+)', pattern) + '$'
		self.re = re.compile(pattern)	
		
	def match(self, request):	
		if self.method == 'all' or request.method.lower() == self.method:	
			result = self.re.match(request.path)
			if result:		
				return result.groupdict()
				
	def resolve(self, args):
		path = self.path
		for arg in args.keys():
			path = re.compile(r'\{%s\}' % arg).sub(str(args[arg]), path)
		return path
		

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Results

class Result:
	
	def do(self, controller):
		pass
	
	
class Render(Result):
	
	def __init__(self, path, args):
		self.path, self.args = path, args if args else {}

	def do(self, controller):
		controller.render_args.update(self.args) 
		controller.render_args.update({
			'session': controller.session,
			'flash': controller.flash
		})
		controller.response.out.write(template.render('views/%s' % self.path, controller.render_args))
		
class Text(Result):
	
	def __init__(self, text):
		self.text = text
		
	def do(self, controller):
		controller.response.out.write(self.text)
		

class Redirect(Result):
	
	def __init__(self, to, args):
		self.to, self.args = to, args
		
	def do(self, controller):
		uri = None
		if callable(self.to):
			uri = controller.router.reverse_route(self.to, self.args if self.args else {})
		else:
			uri = self.to		
		controller.response.set_status(302)
		absolute_url = urlparse.urljoin(controller.request.uri, uri)
		controller.response.headers['Location'] = str(absolute_url)
		controller.response.clear()


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Controller

class Controller:
	
	def _veryBefore(self):
		pass
		
	def _veryAfter(self):
		self.session._store(self)
		self.flash._store(self)

	def render(self, path, args=None):
		return Render(path, args)

	def renderText(self, text):
		return Text(text)
		
	def redirect(self, to, args=None):
		return Redirect(to, args)
		

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Scopes
		
class Request(webapp.Request):
	
	def __init__(self, environ):
		webapp.Request.__init__(self, environ)
		self.domain = self.host[:self.host.index(':')] if self.host.index(':') else self.host


class Response(webapp.Response):
	pass


class Params:
	
	def __init__(self, controller):
		self.controller = controller
	
	def __getitem__(self, key): 
		if self.controller._route_params.has_key(key):
			return self.controller._route_params[key]
		arg = self.controller.request.get(key, allow_multiple=True)
		if len(arg) == 1: arg = arg[0]
		if arg != '' and len(arg) == 0: arg = None
		return arg


class Session(UserDict):

	def __init__(self, controller):
		initialdata = {}
		cookie = controller.request.cookies.get('session')
		if cookie:
			try:
				data = cookie[:cookie.index('-')]
				sign = cookie[cookie.index('-')+1:]
				if sign != self._sign(data):
					raise Exception('Corrupted')
				initialdata = eval(base64.b64decode(data))
			except Exception, e:
				logging.warn('Invalid session : %s' % e)
		UserDict.__init__(self, initialdata)

	def _store(self, controller):
		data = base64.b64encode(str(self))
		sign = self._sign(data)
		controller.response.headers.add_header(
			'Set-Cookie', 'session=%s-%s; path=/;' % (data, sign)
		)
	
	def clear(self):
		self.data.clear()
		
	@classmethod	
	def _sign(cls, str):
		return hmac.new(SECRET_KEY, str, hashlib.sha1).hexdigest()


class Flash(UserDict):
	
	def __init__(self, controller):
		self.out = {}
		initialdata = {}
		cookie = controller.request.cookies.get('flash')
		if cookie:
			try:
				data = cookie[:cookie.index('-')]
				sign = cookie[cookie.index('-')+1:]
				if sign != Session._sign(data):
					raise Exception('Corrupted')
				initialdata = eval(base64.b64decode(data))
			except Exception, e:
				logging.warn('Invalid flash : %s' % e)
		UserDict.__init__(self, initialdata)
		
	def _store(self, controller):
		data = base64.b64encode(str(self.out))
		sign = Session._sign(data)
		controller.response.headers.add_header(
			'Set-Cookie', 'flash=%s-%s; path=/;' % (data, sign)
		)
		
	def __setitem__(self, key, item): 
		self.data[key] = item
		self.out[key] = item
		
	def discard(self, key=None):
		if key:
			self.out.remove(key)
		else:
			self.out = {}
			
	def keep(self, key=None):
		if key:
			self.out[key] = self.get(key)
		else:
			self.out = dict(self)
			
			
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Templates

import django
import django.conf
import django.template

def url_tag(self, context):
	try:
		route = Router.current.route_for_action(self.view_name)
		return route.resolve(self.kwargs)
	except:
		return ''

template._urlnode_render_replacement = url_tag
			

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Error report

def report_error(type, value, tb, response):
	response.set_status(500)
	response.clear()			    
	response.headers['Content-Type'] = 'text/html'
	list = []
	path = os.path.abspath('.') + '/'
	for item in traceback.extract_tb(tb):
		if item[0].startswith(path):
			list.append( (item[0][len(path):], item[1], item[3]) )
	t = django.template.Template("""
		<html>
			<head>
				<title>Error</title>
				<style type="text/css">
					body, html {
						margin: 0;
						padding: 0;
						font-family: sans-serif;
					}
					#error {
						width: 800px;
						margin: 0 auto;
						padding: 20px;
						background: #FFDFDF;
						border: 1px solid #FF8F8F;
						border-top: none;
						border-bottom: none;
					}
					#error h1 {
						color: #c00;
						margin: 0;
					}
					#error h2 {
						margin: 0 0 2em 0;
						font-weight: normal;
						color: #333;
						font-size: 1.2em;
					}
					#error ul {
						padding: 0;
						list-style: none;
					}
					#error li {
						margin-bottom: 1em;
					}
					#error li div {
						background: #fff;
						padding: 3px 0;
						overflow: hidden;
						white-space: nowrap;
					}
					#error .file {
						background: #111;
						color: #fff;
						padding: 3px;
						display: inline-block;
					}
					#error .line {
						background: #ccc;
						padding: 3px;
					}
					#error li:first-child .code {
						color: #c00;
					}
					#error li:first-child {
						font-size: 150%;
					}
				</style>
			</head>
			<body>
				<div id="error">
					<h1>{{ error_name }}</h1>
					<h2>{{ error_description|default:"(no message)" }}</h1>
					<ul>
						{% for trace in traceback reversed %}
							<li>
								<span class="file">{{ trace.0 }}</span>
								<div>
									<span class="line">{{ trace.1 }}</span>
									<span class="code">{{ trace.2 }}</span>
								</div>
							</li>
							{% if forloop.first %}
								<li>Called by :</li>
							{% endif %}
						{% endfor %}
					</ul>
				</div>				
			</body>
		</html>
	""")
	response.out.write(t.render(django.template.Context({
		'error_name': type.__name__ if hasattr(type, '__name__') else str(type),
		'error_description': value,
		'traceback': list
	})))	

	
